# File contains some long lines; breaking them would decrease readability
# pylint: disable=line-too-long,too-many-lines,protected-access
import calendar
import unittest
from unittest.mock import patch
from time import struct_time, mktime

import numpy as np

from Orange.data import ContinuousVariable, TimeVariable, Table, Domain
from Orange.preprocess.discretize import \
    _time_binnings, time_binnings, BinDefinition, Discretizer, FixedWidth, \
    FixedTimeWidth, Binning, \
    TooManyIntervals, SingleValueSql, BinSql


class TestFixedWidth(unittest.TestCase):
    def test_discretization(self):
        x = np.array([[0.21, 0.335, 0, 0.26, np.nan],
                      [0] * 5,
                      [np.nan] * 5]).T
        domain = Domain([ContinuousVariable(f"c{i}") for i in range(x.shape[1])])
        data = Table.from_numpy(domain, x, None)

        dvar = FixedWidth(0.1, 2)(data, 0)
        np.testing.assert_almost_equal(dvar.compute_value.points,
                                       (0.1, 0.2, 0.3))
        self.assertEqual(dvar.values,
                         ('< 0.10', '0.10 - 0.20', '0.20 - 0.30', '≥ 0.30'))

        dvar = FixedWidth(0.2, 1)(data, 0)
        np.testing.assert_almost_equal(dvar.compute_value.points, (0.2, ))
        self.assertEqual(dvar.values, ('< 0.2', '≥ 0.2'))

        dvar = FixedWidth(1, 2)(data, 0)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        dvar = FixedWidth(0.11, 2)(data, 1)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        dvar = FixedWidth(0.11, 2)(data, 2)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        self.assertRaises(TooManyIntervals, FixedWidth(0.0001, 1), data, 0)


class TestFixedTimeWidth(unittest.TestCase):
    def test_discretization(self):
        t = TimeVariable("t")
        x = np.array([[t.to_val("1914"), t.to_val("1945"), np.nan],
                      [t.to_val("1914"), t.to_val("1914"), np.nan],
                      [np.nan, np.nan, np.nan],
                      ]).T
        domain = Domain([t, TimeVariable("t2"), TimeVariable("t3")])
        data = Table.from_numpy(domain, x, None)

        dvar = FixedTimeWidth(10, 1)(data, 1)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        dvar = FixedTimeWidth(10, 2)(data, 2)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        self.assertRaises(TooManyIntervals, FixedWidth(0.0001, 1), data, 0)

        dvar = FixedTimeWidth(10, 0)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(str(y))) for y in (1920, 1930, 1940)])
        self.assertEqual(dvar.values,
                         ('< 1920', '1920 - 1930', '1930 - 1940', '≥ 1940'))

        dvar = FixedTimeWidth(5, 0)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(str(y))) for y in (1915, 1920, 1925, 1930, 1935,
                                             1940, 1945)])
        self.assertEqual(dvar.values,
                         ('< 1915', '1915 - 1920', '1920 - 1925', '1925 - 1930',
                          '1930 - 1935', '1935 - 1940', '1940 - 1945', '≥ 1945')
                         )

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-07-28"), t.to_val("1918-11-11")]]).T)
        dvar = FixedTimeWidth(6, 1)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1915-01-01", "1915-07-01",
                                        "1916-01-01", "1916-07-01",
                                        "1917-01-01", "1917-07-01",
                                        "1918-01-01", "1918-07-01")])

        def tuple_lower(t):
            return tuple(a.lower() for a in t)

        self.assertEqual(tuple_lower(dvar.values),
                         ('< 15 jan', '15 jan - jul', '15 jul - 16 jan',
                          '16 jan - jul', '16 jul - 17 jan', '17 jan - jul',
                          '17 jul - 18 jan', '18 jan - jul', '≥ 18 jul'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-07-28"), t.to_val("1914-11-11")]]).T)
        dvar = FixedTimeWidth(6, 1)(data, 0)
        np.testing.assert_almost_equal(dvar.compute_value.points, [])

        dvar = FixedTimeWidth(2, 1)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-09-01", "1914-11-01")])
        self.assertEqual(tuple_lower(dvar.values), ('< sep', 'sep - nov', '≥ nov'))

        dvar = FixedTimeWidth(1, 1)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-08-01", "1914-09-01",
                                        "1914-10-01", "1914-11-01")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< aug', 'aug - sep', 'sep - oct',
                          'oct - nov', '≥ nov'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-28 10:45"),
                       t.to_val("1914-07-04 15:25")]]).T)
        dvar = FixedTimeWidth(2, 2)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-29", "1914-07-01",
                                        "1914-07-03")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< jun 29', 'jun 29 - jul 01',
                          'jul 01 - jul 03', '≥ jul 03'))

        dvar = FixedTimeWidth(1, 2)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-29", "1914-06-30",
                                        "1914-07-01", "1914-07-02",
                                        "1914-07-03", "1914-07-04")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< jun 29', 'jun 29 - jun 30',
                          'jun 30 - jul 01', 'jul 01 - jul 02',
                          'jul 02 - jul 03', 'jul 03 - jul 04',
                          '≥ jul 04'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-12-30 22:45"),
                       t.to_val("1915-01-02 15:25")]]).T)
        dvar = FixedTimeWidth(1, 2)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-12-31", "1915-01-01",
                                        "1915-01-02")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< 14 dec 31',
                          '14 dec 31 - 15 jan 01',
                          '15 jan 01 - jan 02', '≥ 15 jan 02'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-28 10:45"),
                       t.to_val("1914-06-28 15:25")]]).T)
        dvar = FixedTimeWidth(2, 3)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-28 12:00", "1914-06-28 14:00")])
        self.assertEqual(dvar.values, ('< 12:00', '12:00 - 14:00', '≥ 14:00'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-28 10:45"),
                       t.to_val("1914-06-28 15:25")]]).T)
        dvar = FixedTimeWidth(1, 3)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-28 11:00", "1914-06-28 12:00",
                                        "1914-06-28 13:00", "1914-06-28 14:00",
                                        "1914-06-28 15:00")])
        self.assertEqual(dvar.values, ('< 11:00', '11:00 - 12:00',
                                       '12:00 - 13:00', '13:00 - 14:00',
                                       '14:00 - 15:00', '≥ 15:00'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-28 22:45"),
                       t.to_val("1914-06-29 03:25")]]).T)
        dvar = FixedTimeWidth(1, 3)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-28 23:00", "1914-06-29 00:00",
                                        "1914-06-29 01:00", "1914-06-29 02:00",
                                        "1914-06-29 03:00")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< jun 28 23:00',
                          'jun 28 23:00 - jun 29 00:00',
                          'jun 29 00:00 - 01:00',
                          'jun 29 01:00 - 02:00',
                          'jun 29 02:00 - 03:00',
                          '≥ jun 29 03:00'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-28 22:43"),
                       t.to_val("1914-06-28 23:01")]]).T)
        dvar = FixedTimeWidth(5, 4)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-28 22:45", "1914-06-28 22:50",
                                        "1914-06-28 22:55", "1914-06-28 23:00")])
        self.assertEqual(dvar.values, ('< 22:45', "22:45 - 22:50",
                                       "22:50 - 22:55", "22:55 - 23:00",
                                       '≥ 23:00'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-30 23:48"),
                       t.to_val("1914-07-01 00:06")]]).T)
        dvar = FixedTimeWidth(5, 4)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-30 23:50", "1914-06-30 23:55",
                                        "1914-07-01 00:00", "1914-07-01 00:05")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< jun 30 23:50', "jun 30 23:50 - 23:55",
                          "jun 30 23:55 - jul 01 00:00",
                          "jul 01 00:00 - 00:05", '≥ jul 01 00:05'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-29 23:48"),
                       t.to_val("1914-06-30 00:06")]]).T)
        dvar = FixedTimeWidth(5, 4)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-29 23:50", "1914-06-29 23:55",
                                        "1914-06-30 00:00", "1914-06-30 00:05")])
        self.assertEqual(tuple_lower(dvar.values),
                         ('< jun 29 23:50', "jun 29 23:50 - 23:55",
                          "jun 29 23:55 - jun 30 00:00",
                          "jun 30 00:00 - 00:05", '≥ jun 30 00:05'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-29 23:48:05"),
                       t.to_val("1914-06-29 23:51:59")]]).T)
        dvar = FixedTimeWidth(1, 4)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-29 23:49", "1914-06-29 23:50",
                                        "1914-06-29 23:51")])
        self.assertEqual(dvar.values, ('< 23:49', "23:49 - 23:50",
                                       "23:50 - 23:51", '≥ 23:51'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-06-29 23:48:05.123"),
                       t.to_val("1914-06-29 23:48:33.684")]]).T)
        dvar = FixedTimeWidth(10, 5)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-06-29 23:48:10",
                                        "1914-06-29 23:48:20",
                                        "1914-06-29 23:48:30")])
        self.assertEqual(dvar.values, ('< 23:48:10', "23:48:10 - 23:48:20",
                                       "23:48:20 - 23:48:30", '≥ 23:48:30'))

        data = Table.from_numpy(
            Domain([t]),
            np.array([[t.to_val("1914-12-31 23:59:58.1"),
                       t.to_val("1915-01-01 00:00:01.8")]]).T)
        dvar = FixedTimeWidth(1, 5)(data, 0)
        np.testing.assert_almost_equal(
            dvar.compute_value.points,
            [int(t.to_val(y)) for y in ("1914-12-31 23:59:59",
                                        "1915-01-01 00:00:00",
                                        "1915-01-01 00:00:01")])
        self.assertEqual(dvar.values, ('< 23:59:59', "23:59:59 - 00:00:00",
                                       "00:00:00 - 00:00:01", '≥ 00:00:01'))

        self.assertRaises(TooManyIntervals, FixedTimeWidth(0.0001, 5), data, 0)


class TestBinningDiscretizer(unittest.TestCase):
    def test_no_data(self):
        no_data = Table(Domain([ContinuousVariable("y")]), np.zeros((0, 1)))
        dvar = Binning()(no_data, 0)
        self.assertEqual(dvar.compute_value.points, [])

    @patch("Orange.preprocess.discretize.time_binnings")
    @patch("Orange.preprocess.discretize.decimal_binnings")
    @patch("Orange.preprocess.discretize.Binning._create_binned_var")
    def test_call(self, _, decbin, timebin):
        data = Table(Domain([ContinuousVariable("y"), TimeVariable("t")]),
                     np.array([[1, 2], [3, 4]]))

        Binning(5)(data, 0)
        timebin.assert_not_called()
        self.assertEqual(list(decbin.call_args[0][0]), [1, 3])
        decbin.reset_mock()

        Binning(5)(data, 1)
        decbin.assert_not_called()
        self.assertEqual(list(timebin.call_args[0][0]), [2, 4])

    def test_binning_selection(self):
        var = ContinuousVariable("y")
        discretize = Binning(2)
        # pylint: disable=redefined-outer-name
        create = discretize._create_binned_var

        binnings = []
        self.assertEqual(create(binnings, var).compute_value.points, [])

        binnings = None
        self.assertEqual(create(binnings, var).compute_value.points, [])

        binnings = [
            BinDefinition(np.arange(i + 1),
                          [f"t{x}" for x in range(i + 1)],
                          [f"t{x}" for x in range(i + 1)],
                          1 / i, str(i)
                          )
            for i in (3, 5, 10, 20)
        ]

        for discretize.n in (2, 3):
            self.assertEqual(create(binnings, var).values,
                             ('< t1', "t1 - t2", "≥ t2"))

        for discretize.n in (4, 5, 6, 7):
            self.assertEqual(create(binnings, var).values,
                             ('< t1', "t1 - t2", "t2 - t3", "t3 - t4", "≥ t4"))

        for discretize.n in range(8, 15):
            self.assertEqual(len(create(binnings, var).values), 10)

        for discretize.n in range(16, 25):
            self.assertEqual(len(create(binnings, var).values), 20)


# pylint: disable=redefined-builtin
def create(year=1970, month=1, day=1, hour=0, min=0, sec=0):
    return struct_time((year, month, day, hour, min, sec, 0, 0, 0))


class TestTimeBinning(unittest.TestCase):
    def setUp(self):
        self.dates = [mktime(x) for x in
                      [(1975, 6, 9, 10, 0, 0, 0, 161, 0),
                       (1975, 6, 9, 10, 50, 0, 0, 161, 0),
                       (1975, 6, 9, 11, 40, 0, 0, 161, 0),
                       (1975, 6, 9, 12, 30, 0, 0, 161, 0),
                       (1975, 6, 9, 13, 20, 0, 0, 161, 0),
                       (1975, 6, 9, 14, 10, 0, 0, 161, 0)]]

    def test_binning(self):
        def tr1(s):
            for localname, engname in zip(
                    calendar.month_abbr[1:],
                    "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()):
                s = s.replace(localname, engname)
            return s

        def tr2(ss):
            return list(map(tr1, ss))

        def testbin(start, end):
            bins = _time_binnings(create(*start), create(*end), 3, 51)
            return [(bin.width_label, tr2(bin.short_labels),
                     list(bin.thresholds))
                    for bin in reversed(bins)]

        self.assertEqual(
            testbin((1975, 4, 2), (1989, 3, 1)),
            [('10 years',
              ['1970', '1980', '1990'],
              [0, 315532800, 631152000]),
             ('5 years',
              ['1975', '1980', '1985', '1990'],
              [157766400, 315532800, 473385600, 631152000]),
             ('2 years',
              ['1974', '1976', '1978', '1980', '1982', '1984', '1986', '1988',
               '1990'],
              [126230400, 189302400, 252460800, 315532800, 378691200, 441763200,
               504921600, 567993600, 631152000]),
             ('1 year',
              ['1975', '1976', '1977', '1978', '1979', '1980', '1981', '1982',
               '1983', '1984', '1985', '1986', '1987', '1988', '1989', '1990'],
              [157766400, 189302400, 220924800, 252460800, 283996800, 315532800,
               347155200, 378691200, 410227200, 441763200, 473385600, 504921600,
               536457600, 567993600, 599616000, 631152000]),
             ('6 months',
              ['75 Jan', 'Jul', '76 Jan', 'Jul', '77 Jan', 'Jul', '78 Jan',
               'Jul', '79 Jan', 'Jul', '80 Jan', 'Jul', '81 Jan', 'Jul',
               '82 Jan', 'Jul', '83 Jan', 'Jul', '84 Jan', 'Jul', '85 Jan',
               'Jul', '86 Jan', 'Jul', '87 Jan', 'Jul', '88 Jan', 'Jul',
               '89 Jan', 'Jul'],
              [157766400, 173404800, 189302400, 205027200, 220924800, 236563200,
               252460800, 268099200, 283996800, 299635200, 315532800, 331257600,
               347155200, 362793600, 378691200, 394329600, 410227200, 425865600,
               441763200, 457488000, 473385600, 489024000, 504921600, 520560000,
               536457600, 552096000, 567993600, 583718400, 599616000,
               615254400])]
        )

        self.assertEqual(
            testbin((1975, 4, 2), (1978, 3, 1)),
            [('2 years',
              ['1974', '1976', '1978', '1980'],
              [126230400, 189302400, 252460800, 315532800]),
             ('1 year',
              ['1975', '1976', '1977', '1978', '1979'],
              [157766400, 189302400, 220924800, 252460800, 283996800]),
             ('6 months',
              ['75 Jan', 'Jul',
               '76 Jan', 'Jul',
               '77 Jan', 'Jul',
               '78 Jan', 'Jul'],
              [157766400, 173404800, 189302400, 205027200, 220924800, 236563200,
               252460800, 268099200]),
             ('3 months',
              ['75 Apr', 'Jul', 'Oct',
               '76 Jan', 'Apr', 'Jul', 'Oct',
               '77 Jan', 'Apr', 'Jul', 'Oct',
               '78 Jan', 'Apr'],
              [165542400, 173404800, 181353600, 189302400, 197164800, 205027200,
               212976000, 220924800, 228700800, 236563200, 244512000, 252460800,
               260236800]),
             ('2 months',
              ['75 Mar', 'May', 'Jul', 'Sep', 'Nov',
               '76 Jan', 'Mar', 'May', 'Jul', 'Sep', 'Nov',
               '77 Jan', 'Mar', 'May', 'Jul', 'Sep', 'Nov',
               '78 Jan', 'Mar', 'May'],
              [162864000, 168134400, 173404800, 178761600, 184032000, 189302400,
               194486400, 199756800, 205027200, 210384000, 215654400, 220924800,
               226022400, 231292800, 236563200, 241920000, 247190400, 252460800,
               257558400, 262828800]),
             ('1 month',
              ['75 Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
               '76 Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
               '77 Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
               '78 Jan', 'Feb', 'Mar', 'Apr'],
              [165542400, 168134400, 170812800, 173404800, 176083200, 178761600,
               181353600, 184032000, 186624000, 189302400, 191980800, 194486400,
               197164800, 199756800, 202435200, 205027200, 207705600, 210384000,
               212976000, 215654400, 218246400, 220924800, 223603200, 226022400,
               228700800, 231292800, 233971200, 236563200, 239241600, 241920000,
               244512000, 247190400, 249782400, 252460800, 255139200, 257558400,
               260236800])]
        )

        self.assertEqual(
            testbin((1975, 12, 2), (1976, 1, 3)),
            [('1 month',
              ['75 Dec', '76 Jan', 'Feb'],
              [186624000, 189302400, 191980800]),
             ('2 weeks',
              ['75 Dec 03', '17', '31', '76 Jan 14'],
              [186796800, 188006400, 189216000, 190425600]),
             ('1 week',
              ['75 Dec 03', '10', '17', '24', '31',
               '76 Jan 07'],
              [186796800, 187401600, 188006400, 188611200, 189216000,
               189820800]),
             ('1 day',
              ['75 Dec 02', '03', '04', '05', '06', '07', '08', '09', '10',
               '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21',
               '22', '23', '24', '25', '26', '27', '28', '29', '30', '31',
               '76 Jan 01', '02', '03', '04'],
              [186710400, 186796800, 186883200, 186969600, 187056000, 187142400,
               187228800, 187315200, 187401600, 187488000, 187574400, 187660800,
               187747200, 187833600, 187920000, 188006400, 188092800, 188179200,
               188265600, 188352000, 188438400, 188524800, 188611200, 188697600,
               188784000, 188870400, 188956800, 189043200, 189129600, 189216000,
               189302400, 189388800, 189475200, 189561600])]
        )

        self.assertEqual(
            testbin((1975, 12, 25), (1976, 1, 3)),
            [('1 month',
              ['75 Dec', '76 Jan', 'Feb'],
              [186624000, 189302400, 191980800]),
             ('1 day',
              ['75 Dec 25', '26', '27', '28', '29', '30', '31',
               '76 Jan 01', '02', '03', '04'],
              [188697600, 188784000, 188870400, 188956800, 189043200, 189129600,
               189216000, 189302400, 189388800, 189475200, 189561600]),
             ('12 hours',
              ['75 Dec 25 00:00', '12:00',
               '26 00:00', '12:00',
               '27 00:00', '12:00',
               '28 00:00', '12:00',
               '29 00:00', '12:00',
               '30 00:00', '12:00',
               '31 00:00', '12:00',
               '76 Jan 01 00:00', '12:00',
               '02 00:00', '12:00',
               '03 00:00', '12:00'],
              [188697600, 188740800, 188784000, 188827200, 188870400, 188913600,
               188956800, 189000000, 189043200, 189086400, 189129600, 189172800,
               189216000, 189259200, 189302400, 189345600, 189388800, 189432000,
               189475200, 189518400]),
             ('6 hours',
              ['75 Dec 25 00:00', '06:00', '12:00', '18:00',
               '26 00:00', '06:00', '12:00', '18:00',
               '27 00:00', '06:00', '12:00', '18:00',
               '28 00:00', '06:00', '12:00', '18:00',
               '29 00:00', '06:00', '12:00', '18:00',
               '30 00:00', '06:00', '12:00', '18:00',
               '31 00:00', '06:00', '12:00', '18:00',
               '76 Jan 01 00:00', '06:00', '12:00', '18:00',
               '02 00:00', '06:00', '12:00', '18:00',
               '03 00:00', '06:00'],
              [188697600, 188719200, 188740800, 188762400, 188784000, 188805600,
               188827200, 188848800, 188870400, 188892000, 188913600, 188935200,
               188956800, 188978400, 189000000, 189021600, 189043200, 189064800,
               189086400, 189108000, 189129600, 189151200, 189172800, 189194400,
               189216000, 189237600, 189259200, 189280800, 189302400, 189324000,
               189345600, 189367200, 189388800, 189410400, 189432000, 189453600,
               189475200, 189496800])]
        )

        self.assertEqual(
            testbin((1975, 12, 29), (1976, 1, 3)),
            [('1 month',
              ['75 Dec', '76 Jan', 'Feb'],
              [186624000, 189302400, 191980800]),
             ('1 day',
              ['75 Dec 29', '30', '31',
               '76 Jan 01', '02', '03', '04'],
              [189043200, 189129600, 189216000, 189302400, 189388800, 189475200,
               189561600]),
             ('12 hours',
              ['75 Dec 29 00:00', '12:00',
               '30 00:00', '12:00',
               '31 00:00', '12:00',
               '76 Jan 01 00:00', '12:00',
               '02 00:00', '12:00',
               '03 00:00', '12:00'],
              [189043200, 189086400, 189129600, 189172800, 189216000, 189259200,
               189302400, 189345600, 189388800, 189432000, 189475200,
               189518400]),
             ('6 hours',
              ['75 Dec 29 00:00', '06:00', '12:00', '18:00',
               '30 00:00', '06:00', '12:00', '18:00',
               '31 00:00', '06:00', '12:00', '18:00',
               '76 Jan 01 00:00', '06:00', '12:00', '18:00',
               '02 00:00', '06:00', '12:00', '18:00',
               '03 00:00', '06:00'],
              [189043200, 189064800, 189086400, 189108000, 189129600, 189151200,
               189172800, 189194400, 189216000, 189237600, 189259200, 189280800,
               189302400, 189324000, 189345600, 189367200, 189388800, 189410400,
               189432000, 189453600, 189475200, 189496800]),
             ('3 hours',
              ['75 Dec 29 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '30 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '31 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '76 Jan 01 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '02 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '03 00:00', '03:00'],
              [189043200, 189054000, 189064800, 189075600, 189086400, 189097200,
               189108000, 189118800, 189129600, 189140400, 189151200, 189162000,
               189172800, 189183600, 189194400, 189205200, 189216000, 189226800,
               189237600, 189248400, 189259200, 189270000, 189280800, 189291600,
               189302400, 189313200, 189324000, 189334800, 189345600, 189356400,
               189367200, 189378000, 189388800, 189399600, 189410400, 189421200,
               189432000, 189442800, 189453600, 189464400, 189475200,
               189486000])]
        )

        self.assertEqual(
            testbin((1975, 12, 31, 0, 0, 0), (1976, 1, 1, 14, 30)),
            [('1 day',
              ['75 Dec 31', '76 Jan 01', '02'],
              [189216000, 189302400, 189388800]),
             ('12 hours',
              ['75 Dec 31 00:00', '12:00',
               '76 Jan 01 00:00', '12:00',
               '02 00:00'],
              [189216000, 189259200, 189302400, 189345600, 189388800]),
             ('6 hours',
              ['75 Dec 31 00:00', '06:00', '12:00', '18:00',
               '76 Jan 01 00:00', '06:00', '12:00', '18:00'],
              [189216000, 189237600, 189259200, 189280800, 189302400, 189324000,
               189345600, 189367200]),
             ('3 hours',
              ['75 Dec 31 00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '76 Jan 01 00:00', '03:00', '06:00', '09:00', '12:00', '15:00'],
              [189216000, 189226800, 189237600, 189248400, 189259200, 189270000,
               189280800, 189291600, 189302400, 189313200, 189324000, 189334800,
               189345600, 189356400]),
             ('2 hours',
              ['75 Dec 31 00:00', '02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00',
               '76 Jan 01 00:00', '02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00', '16:00'],
              [189216000, 189223200, 189230400, 189237600, 189244800, 189252000,
               189259200, 189266400, 189273600, 189280800, 189288000, 189295200,
               189302400, 189309600, 189316800, 189324000, 189331200, 189338400,
               189345600, 189352800, 189360000]),
             ('1 hour',
              ['75 Dec 31 00:00', '01:00', '02:00', '03:00', '04:00', '05:00',
               '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', '12:00',
               '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00',
               '20:00', '21:00', '22:00', '23:00',
               '76 Jan 01 00:00', '01:00', '02:00', '03:00', '04:00', '05:00',
               '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', '12:00',
               '13:00', '14:00', '15:00'],
              [189216000, 189219600, 189223200, 189226800, 189230400, 189234000,
               189237600, 189241200, 189244800, 189248400, 189252000, 189255600,
               189259200, 189262800, 189266400, 189270000, 189273600, 189277200,
               189280800, 189284400, 189288000, 189291600, 189295200, 189298800,
               189302400, 189306000, 189309600, 189313200, 189316800, 189320400,
               189324000, 189327600, 189331200, 189334800, 189338400, 189342000,
               189345600, 189349200, 189352800, 189356400])]
        )

        self.assertEqual(
            testbin((1975, 12, 31, 6), (1976, 1, 1)),
            [('1 day',
              ['75 Dec 31', '76 Jan 01', '02'],
              [189216000, 189302400, 189388800]),
             ('12 hours',
              ['75 Dec 31 00:00', '12:00',
               '76 Jan 01 00:00', '12:00'],
              [189216000, 189259200, 189302400, 189345600]),
             ('6 hours',
              ['75 Dec 31 06:00', '12:00', '18:00',
               '76 Jan 01 00:00', '06:00'],
              [189237600, 189259200, 189280800, 189302400, 189324000]),
             ('3 hours',
              ['75 Dec 31 06:00', '09:00', '12:00', '15:00', '18:00', '21:00',
               '76 Jan 01 00:00', '03:00'],
              [189237600, 189248400, 189259200, 189270000, 189280800, 189291600,
               189302400, 189313200]),
             ('2 hours',
              ['75 Dec 31 06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00',
               '76 Jan 01 00:00', '02:00'],
              [189237600, 189244800, 189252000, 189259200, 189266400, 189273600,
               189280800, 189288000, 189295200, 189302400, 189309600]),
             ('1 hour',
              ['75 Dec 31 06:00', '07:00', '08:00', '09:00', '10:00', '11:00',
               '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00',
               '19:00', '20:00', '21:00', '22:00', '23:00',
               '76 Jan 01 00:00', '01:00'],
              [189237600, 189241200, 189244800, 189248400, 189252000, 189255600,
               189259200, 189262800, 189266400, 189270000, 189273600, 189277200,
               189280800, 189284400, 189288000, 189291600, 189295200, 189298800,
               189302400, 189306000]),
             ('30 minutes',
              ['Dec 31 06:00', '06:30', '07:00', '07:30', '08:00', '08:30',
               '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00',
               '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30',
               '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', '19:00',
               '19:30', '20:00', '20:30', '21:00', '21:30', '22:00', '22:30',
               '23:00', '23:30',
               'Jan 01 00:00', '00:30'],
              [189237600, 189239400, 189241200, 189243000, 189244800, 189246600,
               189248400, 189250200, 189252000, 189253800, 189255600, 189257400,
               189259200, 189261000, 189262800, 189264600, 189266400, 189268200,
               189270000, 189271800, 189273600, 189275400, 189277200, 189279000,
               189280800, 189282600, 189284400, 189286200, 189288000, 189289800,
               189291600, 189293400, 189295200, 189297000, 189298800, 189300600,
               189302400, 189304200])]
        )

        self.assertEqual(
            testbin((1975, 12, 31, 23), (1976, 1, 1, 2)),
            [('3 hours',
              ['75 Dec 31 21:00',
               '76 Jan 01 00:00', '03:00'],
              [189291600, 189302400, 189313200]),
             ('2 hours',
              ['75 Dec 31 22:00',
               '76 Jan 01 00:00', '02:00', '04:00'],
              [189295200, 189302400, 189309600, 189316800]),
             ('1 hour',
              ['75 Dec 31 23:00',
               '76 Jan 01 00:00', '01:00', '02:00', '03:00'],
              [189298800, 189302400, 189306000, 189309600, 189313200]),
             ('30 minutes',
              ['Dec 31 23:00', '23:30',
               'Jan 01 00:00', '00:30', '01:00', '01:30', '02:00', '02:30'],
              [189298800, 189300600, 189302400, 189304200, 189306000, 189307800,
               189309600, 189311400]),
             ('15 minutes',
              ['Dec 31 23:00', '23:15', '23:30', '23:45',
               'Jan 01 00:00', '00:15', '00:30', '00:45', '01:00', '01:15',
               '01:30', '01:45', '02:00', '02:15'],
              [189298800, 189299700, 189300600, 189301500, 189302400, 189303300,
               189304200, 189305100, 189306000, 189306900, 189307800, 189308700,
               189309600, 189310500]),
             ('10 minutes',
              ['Dec 31 23:00', '23:10', '23:20', '23:30', '23:40', '23:50',
               'Jan 01 00:00', '00:10', '00:20', '00:30', '00:40', '00:50',
               '01:00', '01:10', '01:20', '01:30', '01:40', '01:50', '02:00',
               '02:10'],
              [189298800, 189299400, 189300000, 189300600, 189301200, 189301800,
               189302400, 189303000, 189303600, 189304200, 189304800, 189305400,
               189306000, 189306600, 189307200, 189307800, 189308400, 189309000,
               189309600, 189310200]),
             ('5 minutes',
              ['Dec 31 23:00', '23:05', '23:10', '23:15', '23:20', '23:25',
               '23:30', '23:35', '23:40', '23:45', '23:50', '23:55',
               'Jan 01 00:00', '00:05', '00:10', '00:15', '00:20', '00:25',
               '00:30', '00:35', '00:40', '00:45', '00:50', '00:55', '01:00',
               '01:05', '01:10', '01:15', '01:20', '01:25', '01:30', '01:35',
               '01:40', '01:45', '01:50', '01:55', '02:00', '02:05'],
              [189298800, 189299100, 189299400, 189299700, 189300000, 189300300,
               189300600, 189300900, 189301200, 189301500, 189301800, 189302100,
               189302400, 189302700, 189303000, 189303300, 189303600, 189303900,
               189304200, 189304500, 189304800, 189305100, 189305400, 189305700,
               189306000, 189306300, 189306600, 189306900, 189307200, 189307500,
               189307800, 189308100, 189308400, 189308700, 189309000, 189309300,
               189309600, 189309900])]
        )

        self.assertEqual(
            testbin((1924, 6, 9, 10), (1924, 6, 9, 23, 18)),
            [('12 hours',
              ['Jun 09 00:00', '12:00', '10 00:00'],
              [-1437868800, -1437825600, -1437782400]),
             ('6 hours',
              ['Jun 09 06:00', '12:00', '18:00', '10 00:00'],
              [-1437847200, -1437825600, -1437804000, -1437782400]),
             ('3 hours',
              ['Jun 09 09:00', '12:00', '15:00', '18:00', '21:00', '10 00:00'],
              [-1437836400, -1437825600, -1437814800, -1437804000, -1437793200,
               -1437782400]),
             ('2 hours',
              ['Jun 09 10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '10 00:00'],
              [-1437832800, -1437825600, -1437818400, -1437811200, -1437804000,
               -1437796800, -1437789600, -1437782400]),
             ('1 hour',
              ['Jun 09 10:00', '11:00', '12:00', '13:00', '14:00', '15:00',
               '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00',
               '23:00',
               '10 00:00'],
              [-1437832800, -1437829200, -1437825600, -1437822000, -1437818400,
               -1437814800, -1437811200, -1437807600, -1437804000, -1437800400,
               -1437796800, -1437793200, -1437789600, -1437786000,
               -1437782400]),
             ('30 minutes',
              ['10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00',
               '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30',
               '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00',
               '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30'],
              [-1437832800, -1437831000, -1437829200, -1437827400, -1437825600,
               -1437823800, -1437822000, -1437820200, -1437818400, -1437816600,
               -1437814800, -1437813000, -1437811200, -1437809400, -1437807600,
               -1437805800, -1437804000, -1437802200, -1437800400, -1437798600,
               -1437796800, -1437795000, -1437793200, -1437791400, -1437789600,
               -1437787800, -1437786000, -1437784200])]
        )

        self.assertEqual(
            testbin((1924, 6, 9, 10), (1924, 6, 9, 13, 18)),
            [('2 hours',
              ['10:00', '12:00', '14:00'],
              [-1437832800, -1437825600, -1437818400]),
             ('1 hour',
              ['10:00', '11:00', '12:00', '13:00', '14:00'],
              [-1437832800, -1437829200, -1437825600, -1437822000,
               -1437818400]),
             ('30 minutes',
              ['10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00',
               '13:30'],
              [-1437832800, -1437831000, -1437829200, -1437827400, -1437825600,
               -1437823800, -1437822000, -1437820200]),
             ('15 minutes',
              ['10:00', '10:15', '10:30', '10:45', '11:00', '11:15', '11:30',
               '11:45', '12:00', '12:15', '12:30', '12:45', '13:00', '13:15',
               '13:30'],
              [-1437832800, -1437831900, -1437831000, -1437830100, -1437829200,
               -1437828300, -1437827400, -1437826500, -1437825600, -1437824700,
               -1437823800, -1437822900, -1437822000, -1437821100,
               -1437820200]),
             ('10 minutes',
              ['10:00', '10:10', '10:20', '10:30', '10:40', '10:50', '11:00',
               '11:10', '11:20', '11:30', '11:40', '11:50', '12:00', '12:10',
               '12:20', '12:30', '12:40', '12:50', '13:00', '13:10', '13:20'],
              [-1437832800, -1437832200, -1437831600, -1437831000, -1437830400,
               -1437829800, -1437829200, -1437828600, -1437828000, -1437827400,
               -1437826800, -1437826200, -1437825600, -1437825000, -1437824400,
               -1437823800, -1437823200, -1437822600, -1437822000, -1437821400,
               -1437820800]),
             ('5 minutes',
              ['10:00', '10:05', '10:10', '10:15', '10:20', '10:25', '10:30',
               '10:35', '10:40', '10:45', '10:50', '10:55', '11:00', '11:05',
               '11:10', '11:15', '11:20', '11:25', '11:30', '11:35', '11:40',
               '11:45', '11:50', '11:55', '12:00', '12:05', '12:10', '12:15',
               '12:20', '12:25', '12:30', '12:35', '12:40', '12:45', '12:50',
               '12:55', '13:00', '13:05', '13:10', '13:15', '13:20'],
              [-1437832800, -1437832500, -1437832200, -1437831900, -1437831600,
               -1437831300, -1437831000, -1437830700, -1437830400, -1437830100,
               -1437829800, -1437829500, -1437829200, -1437828900, -1437828600,
               -1437828300, -1437828000, -1437827700, -1437827400, -1437827100,
               -1437826800, -1437826500, -1437826200, -1437825900, -1437825600,
               -1437825300, -1437825000, -1437824700, -1437824400, -1437824100,
               -1437823800, -1437823500, -1437823200, -1437822900, -1437822600,
               -1437822300, -1437822000, -1437821700, -1437821400, -1437821100,
               -1437820800])]
        )

        self.assertEqual(
            testbin((1924, 6, 9, 10), (1924, 6, 9, 10, 48)),
            [('30 minutes',
              ['10:00', '10:30', '11:00'],
              [-1437832800, -1437831000, -1437829200]),
             ('15 minutes',
              ['10:00', '10:15', '10:30', '10:45', '11:00'],
              [-1437832800, -1437831900, -1437831000, -1437830100,
               -1437829200]),
             ('10 minutes',
              ['10:00', '10:10', '10:20', '10:30', '10:40', '10:50'],
              [-1437832800, -1437832200, -1437831600, -1437831000, -1437830400,
               -1437829800]),
             ('5 minutes',
              ['10:00', '10:05', '10:10', '10:15', '10:20', '10:25', '10:30',
               '10:35', '10:40', '10:45', '10:50'],
              [-1437832800, -1437832500, -1437832200, -1437831900, -1437831600,
               -1437831300, -1437831000, -1437830700, -1437830400, -1437830100,
               -1437829800]),
             ('1 minute',
              ['10:00', '10:01', '10:02', '10:03', '10:04', '10:05', '10:06',
               '10:07', '10:08', '10:09', '10:10', '10:11', '10:12', '10:13',
               '10:14', '10:15', '10:16', '10:17', '10:18', '10:19', '10:20',
               '10:21', '10:22', '10:23', '10:24', '10:25', '10:26', '10:27',
               '10:28', '10:29', '10:30', '10:31', '10:32', '10:33', '10:34',
               '10:35', '10:36', '10:37', '10:38', '10:39', '10:40', '10:41',
               '10:42', '10:43', '10:44', '10:45', '10:46', '10:47', '10:48',
               '10:49'],
              [-1437832800, -1437832740, -1437832680, -1437832620, -1437832560,
               -1437832500, -1437832440, -1437832380, -1437832320, -1437832260,
               -1437832200, -1437832140, -1437832080, -1437832020, -1437831960,
               -1437831900, -1437831840, -1437831780, -1437831720, -1437831660,
               -1437831600, -1437831540, -1437831480, -1437831420, -1437831360,
               -1437831300, -1437831240, -1437831180, -1437831120, -1437831060,
               -1437831000, -1437830940, -1437830880, -1437830820, -1437830760,
               -1437830700, -1437830640, -1437830580, -1437830520, -1437830460,
               -1437830400, -1437830340, -1437830280, -1437830220, -1437830160,
               -1437830100, -1437830040, -1437829980, -1437829920, -1437829860]
              )]
        )

        self.assertEqual(
            testbin((1924, 6, 9, 10), (1924, 6, 9, 10, 20)),
            [('15 minutes',
              ['10:00', '10:15', '10:30'],
              [-1437832800, -1437831900, -1437831000]),
             ('10 minutes',
              ['10:00', '10:10', '10:20', '10:30'],
              [-1437832800, -1437832200, -1437831600, -1437831000]),
             ('5 minutes',
              ['10:00', '10:05', '10:10', '10:15', '10:20', '10:25'],
              [-1437832800, -1437832500, -1437832200, -1437831900, -1437831600,
               -1437831300]),
             ('1 minute',
              ['10:00', '10:01', '10:02', '10:03', '10:04', '10:05', '10:06',
               '10:07', '10:08', '10:09', '10:10', '10:11', '10:12', '10:13',
               '10:14', '10:15', '10:16', '10:17', '10:18', '10:19', '10:20',
               '10:21'],
              [-1437832800, -1437832740, -1437832680, -1437832620, -1437832560,
               -1437832500, -1437832440, -1437832380, -1437832320, -1437832260,
               -1437832200, -1437832140, -1437832080, -1437832020, -1437831960,
               -1437831900, -1437831840, -1437831780, -1437831720, -1437831660,
               -1437831600, -1437831540]),
             ('30 seconds',
              ['10:00:00', '10:00:30', '10:01:00', '10:01:30', '10:02:00',
               '10:02:30', '10:03:00', '10:03:30', '10:04:00', '10:04:30',
               '10:05:00', '10:05:30', '10:06:00', '10:06:30', '10:07:00',
               '10:07:30', '10:08:00', '10:08:30', '10:09:00', '10:09:30',
               '10:10:00', '10:10:30', '10:11:00', '10:11:30', '10:12:00',
               '10:12:30', '10:13:00', '10:13:30', '10:14:00', '10:14:30',
               '10:15:00', '10:15:30', '10:16:00', '10:16:30', '10:17:00',
               '10:17:30', '10:18:00', '10:18:30', '10:19:00', '10:19:30',
               '10:20:00', '10:20:30'],
              [-1437832800, -1437832770, -1437832740, -1437832710, -1437832680,
               -1437832650, -1437832620, -1437832590, -1437832560, -1437832530,
               -1437832500, -1437832470, -1437832440, -1437832410, -1437832380,
               -1437832350, -1437832320, -1437832290, -1437832260, -1437832230,
               -1437832200, -1437832170, -1437832140, -1437832110, -1437832080,
               -1437832050, -1437832020, -1437831990, -1437831960, -1437831930,
               -1437831900, -1437831870, -1437831840, -1437831810, -1437831780,
               -1437831750, -1437831720, -1437831690, -1437831660, -1437831630,
               -1437831600, -1437831570])])

        self.assertEqual(
            testbin((1924, 6, 9, 10, 12, 33), (1924, 6, 9, 10, 18, 12)),
            [('5 minutes',
              ['10:10', '10:15', '10:20'],
              [-1437832200, -1437831900, -1437831600]),
             ('1 minute',
              ['10:12', '10:13', '10:14', '10:15', '10:16', '10:17', '10:18',
               '10:19'],
              [-1437832080, -1437832020, -1437831960, -1437831900, -1437831840,
               -1437831780, -1437831720, -1437831660]),
             ('30 seconds',
              ['10:12:30', '10:13:00', '10:13:30', '10:14:00', '10:14:30',
               '10:15:00', '10:15:30', '10:16:00', '10:16:30', '10:17:00',
               '10:17:30', '10:18:00', '10:18:30'],
              [-1437832050, -1437832020, -1437831990, -1437831960, -1437831930,
               -1437831900, -1437831870, -1437831840, -1437831810, -1437831780,
               -1437831750, -1437831720, -1437831690]),
             ('15 seconds',
              ['10:12:30', '10:12:45', '10:13:00', '10:13:15', '10:13:30',
               '10:13:45', '10:14:00', '10:14:15', '10:14:30', '10:14:45',
               '10:15:00', '10:15:15', '10:15:30', '10:15:45', '10:16:00',
               '10:16:15', '10:16:30', '10:16:45', '10:17:00', '10:17:15',
               '10:17:30', '10:17:45', '10:18:00', '10:18:15'],
              [-1437832050, -1437832035, -1437832020, -1437832005, -1437831990,
               -1437831975, -1437831960, -1437831945, -1437831930, -1437831915,
               -1437831900, -1437831885, -1437831870, -1437831855, -1437831840,
               -1437831825, -1437831810, -1437831795, -1437831780, -1437831765,
               -1437831750, -1437831735, -1437831720, -1437831705]),
             ('10 seconds',
              ['10:12:30', '10:12:40', '10:12:50', '10:13:00', '10:13:10',
               '10:13:20', '10:13:30', '10:13:40', '10:13:50', '10:14:00',
               '10:14:10', '10:14:20', '10:14:30', '10:14:40', '10:14:50',
               '10:15:00', '10:15:10', '10:15:20', '10:15:30', '10:15:40',
               '10:15:50', '10:16:00', '10:16:10', '10:16:20', '10:16:30',
               '10:16:40', '10:16:50', '10:17:00', '10:17:10', '10:17:20',
               '10:17:30', '10:17:40', '10:17:50', '10:18:00', '10:18:10',
               '10:18:20'],
              [-1437832050, -1437832040, -1437832030, -1437832020, -1437832010,
               -1437832000, -1437831990, -1437831980, -1437831970, -1437831960,
               -1437831950, -1437831940, -1437831930, -1437831920, -1437831910,
               -1437831900, -1437831890, -1437831880, -1437831870, -1437831860,
               -1437831850, -1437831840, -1437831830, -1437831820, -1437831810,
               -1437831800, -1437831790, -1437831780, -1437831770, -1437831760,
               -1437831750, -1437831740, -1437831730, -1437831720, -1437831710,
               -1437831700])])

        self.assertEqual(
            testbin((1924, 6, 9, 10, 12, 33), (1924, 6, 9, 10, 13, 12)),
            [('30 seconds',
              ['10:12:30', '10:13:00', '10:13:30'],
              [-1437832050, -1437832020, -1437831990]),
             ('15 seconds',
              ['10:12:30', '10:12:45', '10:13:00', '10:13:15'],
              [-1437832050, -1437832035, -1437832020, -1437832005]),
             ('10 seconds',
              ['10:12:30', '10:12:40', '10:12:50', '10:13:00', '10:13:10',
               '10:13:20'],
              [-1437832050, -1437832040, -1437832030, -1437832020, -1437832010,
               -1437832000]),
             ('5 seconds',
              ['10:12:30', '10:12:35', '10:12:40', '10:12:45', '10:12:50',
               '10:12:55', '10:13:00', '10:13:05', '10:13:10', '10:13:15'],
              [-1437832050, -1437832045, -1437832040, -1437832035, -1437832030,
               -1437832025, -1437832020, -1437832015, -1437832010,
               -1437832005]),
             ('1 second',
              ['10:12:33', '10:12:34', '10:12:35', '10:12:36', '10:12:37',
               '10:12:38', '10:12:39', '10:12:40', '10:12:41', '10:12:42',
               '10:12:43', '10:12:44', '10:12:45', '10:12:46', '10:12:47',
               '10:12:48', '10:12:49', '10:12:50', '10:12:51', '10:12:52',
               '10:12:53', '10:12:54', '10:12:55', '10:12:56', '10:12:57',
               '10:12:58', '10:12:59', '10:13:00', '10:13:01', '10:13:02',
               '10:13:03', '10:13:04', '10:13:05', '10:13:06', '10:13:07',
               '10:13:08', '10:13:09', '10:13:10', '10:13:11', '10:13:12',
               '10:13:13'],
              [-1437832047, -1437832046, -1437832045, -1437832044, -1437832043,
               -1437832042, -1437832041, -1437832040, -1437832039, -1437832038,
               -1437832037, -1437832036, -1437832035, -1437832034, -1437832033,
               -1437832032, -1437832031, -1437832030, -1437832029, -1437832028,
               -1437832027, -1437832026, -1437832025, -1437832024, -1437832023,
               -1437832022, -1437832021, -1437832020, -1437832019, -1437832018,
               -1437832017, -1437832016, -1437832015, -1437832014, -1437832013,
               -1437832012, -1437832011, -1437832010, -1437832009, -1437832008,
               -1437832007])])

        self.assertEqual(
            testbin((1973, 9, 14), (2010, 9, 8)),
            [
                ('50 years',
                 ['1950', '2000', '2050'],
                 [-631152000, 946684800, 2524608000]),
                ('25 years',
                 ['1950', '1975', '2000', '2025'],
                 [-631152000, 157766400, 946684800, 1735689600]),
                ('10 years',
                 ['1970', '1980', '1990', '2000', '2010', '2020'],
                 [0, 315532800, 631152000, 946684800, 1262304000, 1577836800]),
                ('5 years',
                 ['1970', '1975', '1980', '1985', '1990', '1995', '2000',
                  '2005', '2010', '2015'],
                 [0, 157766400, 315532800, 473385600, 631152000, 788918400,
                  946684800, 1104537600, 1262304000, 1420070400]),
                ('2 years',
                 ['1972', '1974', '1976', '1978', '1980', '1982', '1984',
                  '1986', '1988', '1990', '1992', '1994', '1996', '1998',
                  '2000', '2002', '2004', '2006', '2008', '2010', '2012'],
                 [63072000, 126230400, 189302400, 252460800, 315532800,
                  378691200, 441763200, 504921600, 567993600, 631152000,
                  694224000, 757382400, 820454400, 883612800, 946684800,
                  1009843200, 1072915200, 1136073600, 1199145600, 1262304000,
                  1325376000]),
                ('1 year',
                 ['1973', '1974', '1975', '1976', '1977', '1978', '1979',
                  '1980', '1981', '1982', '1983', '1984', '1985', '1986',
                  '1987', '1988', '1989', '1990', '1991', '1992', '1993',
                  '1994', '1995', '1996', '1997', '1998', '1999', '2000',
                  '2001', '2002', '2003', '2004', '2005', '2006', '2007',
                  '2008', '2009', '2010', '2011'],
                 [94694400, 126230400, 157766400, 189302400, 220924800,
                  252460800, 283996800, 315532800, 347155200, 378691200,
                  410227200, 441763200, 473385600, 504921600, 536457600,
                  567993600, 599616000, 631152000, 662688000, 694224000,
                  725846400, 757382400, 788918400, 820454400, 852076800,
                  883612800, 915148800, 946684800, 978307200, 1009843200,
                  1041379200, 1072915200, 1104537600, 1136073600, 1167609600,
                  1199145600, 1230768000, 1262304000, 1293840000])
            ]
        )

    def test_min_unique(self):
        bins = time_binnings(self.dates, min_unique=7)
        self.assertEqual(len(bins), 1)
        np.testing.assert_equal(bins[0].thresholds[:-1], self.dates)

    def test_add_unique(self):
        bins = time_binnings(self.dates, add_unique=7)
        self.assertNotEqual(len(bins), 1)
        np.testing.assert_equal(bins[0].thresholds[:-1], self.dates)

    def test_limits(self):
        self.assertEqual(
            {b.nbins
             for b in time_binnings(self.dates, min_bins=9, max_bins=17)},
            {9, 17})
        self.assertEqual(
            {b.nbins
             for b in time_binnings(self.dates, min_bins=9, max_bins=16)},
            {9})
        self.assertEqual(
            {b.nbins
             for b in time_binnings(self.dates, min_bins=10, max_bins=17)},
            {17})

    def test_single_value(self):
        dates = np.array([42])
        bins = time_binnings(dates)
        self.assertEqual(len(bins), 1)
        np.testing.assert_equal(bins[0].thresholds, [42, 43])

    def test_multiple_identical(self):
        dates = np.array([42] * 5)
        bins = time_binnings(dates)
        self.assertEqual(len(bins), 1)
        np.testing.assert_equal(bins[0].thresholds, [42, 43])

    def test_no_values(self):
        dates = np.array([])
        self.assertRaises(ValueError, time_binnings, dates)

        dates = np.array([np.nan, np.nan])
        self.assertRaises(ValueError, time_binnings, dates)

    def test_before_epoch(self):
        hour = 24 * 60 * 60
        dates = [-hour, 0, hour,]
        bins = time_binnings(dates)
        self.assertEqual(list(bins[0].thresholds), [-hour, 0, hour, 2 * hour])


class TestBinDefinition(unittest.TestCase):
    def test_labels(self):
        thresholds = np.array([1, 2, 3.14])
        self.assertEqual(BinDefinition(thresholds).labels,
                         ["1", "2", "3.14"])
        self.assertEqual(BinDefinition(thresholds, "%.3f").labels,
                         ["1.000", "2.000", "3.140"])
        self.assertEqual(BinDefinition(thresholds, lambda x: f"b{x:g}").labels,
                         ["b1", "b2", "b3.14"])
        self.assertEqual(BinDefinition(thresholds, list("abc")).labels,
                         list("abc"))

    def test_width_label(self):
        thresholds = np.array([1, 2, 3.14])
        self.assertEqual(BinDefinition(thresholds).width_label, "")
        self.assertEqual(BinDefinition(thresholds, width=3).width_label, "3")
        self.assertEqual(BinDefinition(thresholds, width=3.14).width_label, "3.14")

    def test_thresholds(self):
        thresholds = np.array([1, 2, 3.14])
        bindef = BinDefinition(thresholds)
        np.testing.assert_equal(bindef.thresholds, thresholds)
        self.assertEqual(bindef.start, 1)
        self.assertEqual(bindef.nbins, 2)


class TestDiscretizer(unittest.TestCase):
    def test_equality(self):
        v1 = ContinuousVariable("x")
        v2 = ContinuousVariable("x", number_of_decimals=42)
        v3 = ContinuousVariable("y")
        assert v1 == v2

        t1 = Discretizer(v1, [0, 2, 1])
        t1a = Discretizer(v2, [0, 2, 1])
        t2 = Discretizer(v3, [0, 2, 1])
        self.assertEqual(t1, t1)
        self.assertEqual(t1, t1a)
        self.assertNotEqual(t1, t2)

        self.assertEqual(hash(t1), hash(t1a))
        self.assertNotEqual(hash(t1), hash(t2))

        t1 = Discretizer(v1, [0, 2, 1])
        t1a = Discretizer(v2, [1, 2, 0])
        self.assertNotEqual(t1, t1a)
        self.assertNotEqual(hash(t1), hash(t1a))

    def test_fmt_interval(self):
        def fmt(x):
            return f"{x:.2f}"

        f = Discretizer._fmt_interval
        self.assertEqual(f(1, 2, str), "1 - 2")
        self.assertEqual(f(1, 2, fmt), "1 - 2")
        self.assertEqual(f(1, 2, fmt, strip_zeros=False), "1.00 - 2.00")

        self.assertEqual(f(-np.inf, 2, fmt), "< 2")
        self.assertEqual(f(-np.inf, 2, fmt, strip_zeros=False), "< 2.00")
        self.assertEqual(f(None, 2, fmt), "< 2")
        self.assertEqual(f(None, 2, fmt, strip_zeros=False), "< 2.00")

        self.assertEqual(f(2, np.inf, fmt), "≥ 2")
        self.assertEqual(f(2, np.inf, fmt, strip_zeros=False), "≥ 2.00")
        self.assertEqual(f(2, None, fmt), "≥ 2")
        self.assertEqual(f(2, None, fmt, strip_zeros=False), "≥ 2.00")

        with self.assertRaises(ValueError):
            f(1.122, 1.123, fmt)


    def test_get_labels(self):
        points = [2.46, 2.68, 2.794]
        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.1f}", points),
            ['< 2.5', '2.5 - 2.7', '2.7 - 2.8', '≥ 2.8']
        )
        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.2f}", points),
            ['< 2.46', '2.46 - 2.68', '2.68 - 2.79', '≥ 2.79']
        )
        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.4f}", points),
            ['< 2.46', '2.46 - 2.68', '2.68 - 2.794', '≥ 2.794']
        )

        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.4f}", points,
                                    strip_zeros=False),
            ['< 2.4600', '2.4600 - 2.6800', '2.6800 - 2.7940', '≥ 2.7940']
        )

        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.4f}", [100, 200]),
            ['< 100', '100 - 200', '≥ 200']
        )

        self.assertEqual(
            Discretizer._get_labels(lambda x: f"{x:.4f}", [0]),
            ['< 0', '≥ 0']
        )

    def test_get_discretized_values_empty(self):
        x = ContinuousVariable("x")
        d = Discretizer(x, [])
        points, values, to_sql = d._get_discretized_values(None, np.array([]))
        self.assertEqual(len(points), 0)
        self.assertEqual(values, ["single_value"])
        self.assertIsInstance(to_sql, SingleValueSql)

    def test_get_discretized_values_identical_points(self):
        x = ContinuousVariable("x")
        with self.assertRaises(ValueError):
            Discretizer._get_discretized_values(x, np.array([0, 1, 1, 2]))

    def test_get_discretized_values_no_ndigits(self):
        x = ContinuousVariable("x", number_of_decimals=2)
        points, values, to_sql \
            = Discretizer._get_discretized_values(x, np.array([1, 2, 3, 4]))
        np.testing.assert_equal(points, [1, 2, 3, 4])
        self.assertEqual(values, ['< 1', '1 - 2', '2 - 3', '3 - 4', '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x, np.array([1, 2.1, 3, 4]))
        np.testing.assert_equal(points, [1, 2.1, 3, 4])
        self.assertEqual(values, ['< 1', '1 - 2.1', '2.1 - 3', '3 - 4', '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x,
                                                  np.array([1, 2.1, 3.1234, 4]))
        np.testing.assert_equal(points, [1, 2.1, 3.1234, 4])
        self.assertEqual(values, ['< 1', '1 - 2.1', '2.1 - 3.1234', '3.1234 - 4', '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x,
                                                  np.array([1,
                                                            2.1000000001,
                                                            2.1000000002,
                                                            4]))
        np.testing.assert_equal(points, [1, 2.1000000001, 2.1000000002, 4])
        self.assertEqual(values, ['< 1',
                                  '1 - 2.1000000001',
                                  '2.1000000001 - 2.1000000002',
                                  '2.1000000002 - 4',
                                  '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x,
                                                  np.array([1,
                                                            2.1000000001,
                                                            2.1000000002,
                                                            2.1000000003]))
        np.testing.assert_equal(points,
                                [1, 2.1000000001, 2.1000000002, 2.1000000003])
        self.assertEqual(values, ['< 1',
                                  '1 - 2.1000000001',
                                  '2.1000000001 - 2.1000000002',
                                  '2.1000000002 - 2.1000000003',
                                  '≥ 2.1000000003'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x,
                                                  np.array([1,
                                                            2.1000000001,
                                                            2.100000000211,
                                                            2.1000000003]))
        np.testing.assert_equal(points,
                                [1, 2.1000000001, 2.1000000002, 2.1000000003])
        self.assertEqual(values, ['< 1',
                                  '1 - 2.1000000001',
                                  '2.1000000001 - 2.1000000002',
                                  '2.1000000002 - 2.1000000003',
                                  '≥ 2.1000000003'])
        self.assertIsInstance(to_sql, BinSql)

    def test_get_discretized_values_round_builtin_vs_numpy(self):
        x = ContinuousVariable("x", number_of_decimals=0)
        points, values, _ \
            = Discretizer._get_discretized_values(x,
                                                  np.array([2.3455,
                                                            2.346]))
        np.testing.assert_equal(points,
                                [2.345, 2.346])
        self.assertEqual(values, ['< 2.345',
                                  '2.345 - 2.346',
                                  '≥ 2.346'])

        points, values, _ \
            = Discretizer._get_discretized_values(x,
                                                  np.array([2.1345,
                                                            2.135]))
        np.testing.assert_equal(points,
                                [2.1345, 2.135])
        self.assertEqual(values, ['< 2.1345',
                                  '2.1345 - 2.135',
                                  '≥ 2.135'])

    def test_get_discretized_values_with_ndigits(self):
        x = ContinuousVariable("x")
        apoints = [1, 2, 3, 4]
        points, values, to_sql \
            = Discretizer._get_discretized_values(x, apoints, ndigits=0)
        np.testing.assert_array_equal(points, np.array([1, 2, 3, 4]))
        self.assertEqual(values, ['< 1', '1 - 2', '2 - 3', '3 - 4', '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)

        points, values, to_sql \
            = Discretizer._get_discretized_values(x, apoints, ndigits=2)
        np.testing.assert_array_equal(points, np.array([1, 2, 3, 4]))
        self.assertEqual(values, ['< 1.00', '1.00 - 2.00', '2.00 - 3.00',
                                  '3.00 - 4.00', '≥ 4.00'])
        self.assertIsInstance(to_sql, BinSql)

        apoints = [1, 2.1234, 2.1345, 4]
        points, values, to_sql \
            = Discretizer._get_discretized_values(x, apoints, ndigits=1)
        np.testing.assert_array_equal(points, np.array([1, 2.12, 2.13, 4]))
        self.assertEqual(values,
                         ['< 1', '1 - 2.12', '2.12 - 2.13', '2.13 - 4', '≥ 4'])
        self.assertIsInstance(to_sql, BinSql)


if __name__ == '__main__':
    unittest.main()
